{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "aedb7e8f",
   "metadata": {},
   "source": [
    "# Counterfactual explanations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "a734bee1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import trustyai\n",
    "\n",
    "trustyai.init(\n",
    "    # JVM settings specific for mybinder.org's limitations\n",
    "    \"-Xmx128m\", \"-XX:+UseG1GC\", \"-XX:MaxGCPauseMillis=1000\",\n",
    "    path=[\n",
    "        \"../dep/org/kie/kogito/explainability-core/1.8.0.Final/*\",\n",
    "        \"../dep/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar\",\n",
    "        \"../dep/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar\",\n",
    "        \"../dep/org/optaplanner/optaplanner-core/8.8.0.Final/optaplanner-core-8.8.0.Final.jar\",\n",
    "        \"../dep/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar\",\n",
    "        \"../dep/org/kie/kie-api/7.55.0.Final/kie-api-7.55.0.Final.jar\",\n",
    "        \"../dep/io/micrometer/micrometer-core/1.6.6/micrometer-core-1.6.6.jar\",\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62849953",
   "metadata": {},
   "source": [
    "## Simple example\n",
    "\n",
    "We start by defining our black-box model, typically represented by\n",
    "\n",
    "$$\n",
    "f(\\mathbf{x}) = \\mathbf{y}\n",
    "$$\n",
    "\n",
    "Where $\\mathbf{x}=\\{x_1, x_2, \\dots,x_m\\}$ and $\\mathbf{y}=\\{y_1, y_2, \\dots,y_n\\}$.\n",
    "\n",
    "Our example toy model, in this case, takes an all-numerical input $\\mathbf{x}$ and return a $\\mathbf{y}$ of either `true` or `false` if the sum of the $\\mathbf{x}$ components is within a threshold $\\epsilon$ of a point $\\mathbf{C}$, that is:\n",
    "\n",
    "$$\n",
    "f(\\mathbf{x}, \\epsilon, \\mathbf{C})=\\begin{cases}\n",
    "\\text{true},\\qquad \\text{if}\\ \\mathbf{C}-\\epsilon<\\sum_{i=1}^m x_i <\\mathbf{C}+\\epsilon \\\\\n",
    "\\text{false},\\qquad \\text{otherwise}\n",
    "\\end{cases}\n",
    "$$\n",
    "\n",
    "This model is provided in the `TestUtils` module. We instantiate with a $\\mathbf{C}=500$ and $\\epsilon=1.0$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f9e9630c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from trustyai.utils import TestUtils\n",
    "\n",
    "center = 500.0\n",
    "epsilon = 1.0\n",
    "\n",
    "model = TestUtils.getSumThresholdModel(center, epsilon)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b08943c5",
   "metadata": {},
   "source": [
    "Next we need to define a **goal**.\n",
    "If our model is $f(\\mathbf{x'})=\\mathbf{y'}$ we are then defining our $\\mathbf{y'}$ and the counterfactual result will be the $\\mathbf{x'}$ which satisfies $f(\\mathbf{x'})=\\mathbf{y'}$.\n",
    "\n",
    "We will define our goal as `true`, that is, the sum is withing the vicinity of a (to be defined) point $\\mathbf{C}$. The goal is a list of `Output` which take the following parameters\n",
    "\n",
    "- The feature name\n",
    "- The feature type\n",
    "- The feature value (wrapped in `Value`)\n",
    "- A confidence threshold, which we will leave at zero (no threshold)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ac0f09d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from trustyai.model import Output, Type, Value\n",
    "\n",
    "goal = [Output(\"inside\", Type.BOOLEAN, Value(True), 0.0)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9107ced8",
   "metadata": {},
   "source": [
    "We will now define our initial features, $\\mathbf{x}$. Each feature can be instantiated by using `FeatureFactory` and in this case we want to use numerical features, so we'll use `FeatureFactory.newNumericalFeature`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "73da2506",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "from trustyai.model import FeatureFactory\n",
    "\n",
    "features = [\n",
    "    FeatureFactory.newNumericalFeature(f\"x{i+1}\", random.random() * 10.0)\n",
    "    for i in range(4)\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd4496a5",
   "metadata": {},
   "source": [
    "As we can see, the sum of of the features will not be within $\\epsilon$ (1.0) of $\\mathbf{C}$ (500.0). As such the model prediction will be `false`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "7c23b579",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Feature x1 has value 7.242303576013502\n",
      "Feature x2 has value 6.891082170610017\n",
      "Feature x3 has value 2.82558053318596\n",
      "Feature x4 has value 3.643981984322476\n",
      "\n",
      "Features sum is 20.602948264131957\n"
     ]
    }
   ],
   "source": [
    "feature_sum = 0.0\n",
    "for f in features:\n",
    "    value = f.getValue().asNumber()\n",
    "    print(f\"Feature {f.getName()} has value {value}\")\n",
    "    feature_sum += value\n",
    "print(f\"\\nFeatures sum is {feature_sum}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8fbe07c",
   "metadata": {},
   "source": [
    "The next step is to specify the **constraints** of the features, i.e. which features can be changed and which should be fixed. Since we want all features to be able to change, we specify `False` for all of them:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "bcff1d11",
   "metadata": {},
   "outputs": [],
   "source": [
    "constraints = [False] * 4"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ee4466b",
   "metadata": {},
   "source": [
    "Finally, we also specify which are the **bounds** for the counterfactual search. Typically this can be set either using domain-specific knowledge or taken from the data. In this case we simply specify an arbitrary (sensible) value, e.g. all the features can vary between `0` and `1000`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "a68b9266",
   "metadata": {},
   "outputs": [],
   "source": [
    "from trustyai.model.domain import NumericalFeatureDomain\n",
    "\n",
    "feature_boundaries = [NumericalFeatureDomain.create(0.0, 1000.0)] * 4"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e61ef262",
   "metadata": {},
   "source": [
    "In order to use the boundaries in the explainer we need to wrap all of them in a `DataDomain` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "8b244713",
   "metadata": {},
   "outputs": [],
   "source": [
    "from trustyai.model import DataDomain\n",
    "\n",
    "data_domain = DataDomain(feature_boundaries)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8021162b",
   "metadata": {},
   "source": [
    "We can now instantiate the **explainer** itself.\n",
    "\n",
    "To do so, we will to configure the termination criteria. For this example we will specify that the counterfactual search should only execute a maximum of 10,000 iterations before stopping and returning whatever the best result is so far."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "79051645",
   "metadata": {},
   "outputs": [],
   "source": [
    "from org.optaplanner.core.config.solver.termination import TerminationConfig\n",
    "from org.kie.kogito.explainability.local.counterfactual import (\n",
    "    CounterfactualConfigurationFactory,\n",
    ")\n",
    "from java.lang import Long\n",
    "\n",
    "termination_config = TerminationConfig().withScoreCalculationCountLimit(\n",
    "    Long.valueOf(10_000)\n",
    ")\n",
    "\n",
    "solver_config = (\n",
    "    CounterfactualConfigurationFactory.builder()\n",
    "    .withTerminationConfig(termination_config)\n",
    "    .build()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c450a689",
   "metadata": {},
   "source": [
    "We can can now instantiate the explainer itself using `CounterfactualExplainer` and our `solver_config` configuration."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "4ae12616",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "SLF4J: Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".\n",
      "SLF4J: Defaulting to no-operation (NOP) logger implementation\n",
      "SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.\n"
     ]
    }
   ],
   "source": [
    "from trustyai.explainers import CounterfactualExplainer\n",
    "\n",
    "explainer = CounterfactualExplainer(solver_config)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5107e595",
   "metadata": {},
   "source": [
    "We will now express the counterfactual problem as defined above.\n",
    "\n",
    "- `original` represents our $\\mathbf{x}$ which know gives a prediction of `False`\n",
    "- `goals` represents our $\\mathbf{y'}$, that is our desired prediction (`True`)\n",
    "- `domain` repreents the boundaries for the counterfactual search"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "a5cf01d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from trustyai.model import PredictionFeatureDomain, PredictionInput, PredictionOutput\n",
    "\n",
    "original = PredictionInput(features)\n",
    "goals = PredictionOutput(goal)\n",
    "domain = PredictionFeatureDomain(data_domain.getFeatureDomains())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b84a42c5",
   "metadata": {},
   "source": [
    "We wrap these quantities in a `CounterfactualPrediction` (the UUID is simply to label the search instance):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "0c52e4f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import uuid\n",
    "from trustyai.model import CounterfactualPrediction\n",
    "\n",
    "prediction = CounterfactualPrediction(\n",
    "    original, goals, domain, constraints, None, uuid.uuid4()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b49829e",
   "metadata": {},
   "source": [
    "We now request the counterfactual $\\mathbf{x'}$ which is closest to $\\mathbf{x}$ and which satisfies $f(\\mathbf{x'}, \\epsilon, \\mathbf{C})=\\mathbf{y'}$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "f479771d",
   "metadata": {},
   "outputs": [],
   "source": [
    "explanation = explainer.explain(prediction, model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7b6770a8",
   "metadata": {},
   "source": [
    "We can see that the counterfactual $\\mathbf{x'}$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "6c060497",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "java.lang.DoubleFeature{value=486.00256060387716, intRangeMinimum=0.0, intRangeMaximum=1000.0, id='x1'}\n",
      "java.lang.DoubleFeature{value=6.5324783965063915, intRangeMinimum=0.0, intRangeMaximum=1000.0, id='x2'}\n",
      "java.lang.DoubleFeature{value=2.82558053318596, intRangeMinimum=0.0, intRangeMaximum=1000.0, id='x3'}\n",
      "java.lang.DoubleFeature{value=3.643981984322476, intRangeMinimum=0.0, intRangeMaximum=1000.0, id='x4'}\n",
      "\n",
      "Feature sum is 499.004601517892\n"
     ]
    }
   ],
   "source": [
    "feature_sum = 0.0\n",
    "for entity in explanation.getEntities():\n",
    "    print(entity)\n",
    "    feature_sum += entity.getProposedValue()\n",
    "\n",
    "print(f\"\\nFeature sum is {feature_sum}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "713da382",
   "metadata": {},
   "source": [
    "### Constrained features\n",
    "\n",
    "As we've seen, it is possible to constraint a specific feature $x_i$ by setting the _constraints_ list corresponding element to `True`.\n",
    "\n",
    "In this example, we know want to fix $x_1$ and $x_4$. That is, these features should have the same value in the counterfactual $\\mathbf{x'}$ as in the original $\\mathbf{x}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "af59baf3",
   "metadata": {},
   "outputs": [],
   "source": [
    "constraints = [True, False, False, True]  # x1, x2, x3 and x4"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a6fec4d",
   "metadata": {},
   "source": [
    "We simply need to wrap the previous quantities with the new constraints:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "906d02e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "prediction = CounterfactualPrediction(\n",
    "    original, goals, domain, constraints, None, uuid.uuid4()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a20da7bb",
   "metadata": {},
   "source": [
    "And request a new counterfactual explanation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "f67e081d",
   "metadata": {},
   "outputs": [],
   "source": [
    "explanation = explainer.explain(prediction, model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6d2b808b",
   "metadata": {},
   "source": [
    "We can see that $x_1$ and $x_4$ has the same value as the original and the model satisfies the conditions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "d99b80b8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original x1: 7.242303576013502\n",
      "Original x4: 3.643981984322476\n",
      "\n",
      "java.lang.DoubleFeature{value=7.242303576013502, intRangeMinimum=7.242303576013502, intRangeMaximum=7.242303576013502, id='x1'}\n",
      "java.lang.DoubleFeature{value=7.8052010132971805, intRangeMinimum=0.0, intRangeMaximum=1000.0, id='x2'}\n",
      "java.lang.DoubleFeature{value=481.34447531284985, intRangeMinimum=0.0, intRangeMaximum=1000.0, id='x3'}\n",
      "java.lang.DoubleFeature{value=3.643981984322476, intRangeMinimum=3.643981984322476, intRangeMaximum=3.643981984322476, id='x4'}\n"
     ]
    }
   ],
   "source": [
    "print(f\"Original x1: {features[0].getValue()}\")\n",
    "print(f\"Original x4: {features[3].getValue()}\\n\")\n",
    "\n",
    "for entity in explanation.getEntities():\n",
    "    print(entity)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d54e81de",
   "metadata": {},
   "source": [
    "## Using Python models\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "147b1915",
   "metadata": {},
   "source": [
    "We will now show how to use a custom Python model with TrustyAI counterfactual explanations.\n",
    "\n",
    "The model will be an [XGBoost](https://github.com/dmlc/xgboost) one trained with the `credit-bias` dataset (available [here](https://github.com/ruivieira/benchmark-models/tree/main/credit-bias)).\n",
    "\n",
    "For convenience, the model is pre-trained and serialised with `joblib` so that for this example we simply need to deserialised it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "6986a0ed",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,\n",
      "              colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1,\n",
      "              importance_type='gain', interaction_constraints='',\n",
      "              learning_rate=0.07, max_delta_step=0, max_depth=8,\n",
      "              min_child_weight=1, missing=nan, monotone_constraints='()',\n",
      "              n_estimators=200, n_jobs=12, num_parallel_tree=1, random_state=27,\n",
      "              reg_alpha=0, reg_lambda=1, scale_pos_weight=0.9861206227457426,\n",
      "              seed=27, subsample=1, tree_method='exact', validate_parameters=1,\n",
      "              verbosity=None)\n"
     ]
    }
   ],
   "source": [
    "import joblib\n",
    "\n",
    "xg_model = joblib.load(\"models/credit-bias-xgboost.joblib\")\n",
    "print(xg_model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fdc2144c",
   "metadata": {},
   "source": [
    "This model has as a single **output** a boolean `PaidLoan`, which will predict whether a certain loan applicant will repay the loan in time or not. The model is slightly more complex than the previous examples, with **input** features:\n",
    "\n",
    "|Input feature         | Type    | Note        |\n",
    "|----------------------|---------|-------------|\n",
    "|`NewCreditCustomer`   |boolean  ||\n",
    "|`Amount`              |numerical||\n",
    "|`Interest`            |numerical||\n",
    "|`LoanDuration`        |numerical|In months|\n",
    "|`Education`           |numerical|Level (1, 2, 3..)|\n",
    "|`NrOfDependants`      |numerical|Integer|\n",
    "|`EmploymentDurationCurrentEmployer`|numerical|Integer (years)|\n",
    "|`IncomeFromPrincipalEmployer`|numerical||\n",
    "|`IncomeFromPension`   |numerical||\n",
    "|`IncomeFromFamilyAllowance`|numerical||\n",
    "|`IncomeFromSocialWelfare`|numerical||\n",
    "|`IncomeFromLeavePay`|numerical||\n",
    "|`IncomeFromChildSupport`|numerical||\n",
    "|`IncomeOther`|numerical||\n",
    "|`ExistingLiabilities`|numerical|integer|\n",
    "|`RefinanceLiabilities`|numerical|integer|\n",
    "|`DebtToIncome`|numerical||\n",
    "|`FreeCash`|numerical||\n",
    "|`CreditScoreEeMini`|numerical|integer|\n",
    "|`NoOfPreviousLoansBeforeLoan`|numerical|integer|\n",
    "|`AmountOfPreviousLoansBeforeLoan`|numerical||\n",
    "|`PreviousRepaymentsBeforeLoan`|numerical||\n",
    "|`PreviousEarlyRepaymentsBefoleLoan`|numerical||\n",
    "|`PreviousEarlyRepaymentsCountBeforeLoan`|numerical|integer|\n",
    "|`Council_house`|boolean||\n",
    "|`Homeless`|boolean||\n",
    "|`Joint_ownership`|boolean||\n",
    "|`Joint_tenant`|boolean||\n",
    "|`Living_with_parents`|boolean||\n",
    "|`Mortgage`|boolean||\n",
    "|`Other`|boolean||\n",
    "|`Owner`|boolean||\n",
    "|`Owner_with_encumbrance`|boolean||\n",
    "|`Tenant`|boolean||\n",
    "|`Entrepreneur`|boolean||\n",
    "|`Fully`|boolean||\n",
    "|`Partially`|boolean||\n",
    "|`Retiree`|boolean||\n",
    "|`Self_employed`|boolean||\n",
    "\n",
    "We will start by testing the model with an input we are quite sure (from the original data) that will be predicted as `false`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "550c1ef7",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = [\n",
    "    [\n",
    "        False,\n",
    "        2125.0,\n",
    "        20.97,\n",
    "        60,\n",
    "        4.0,\n",
    "        0.0,\n",
    "        6.0,\n",
    "        0.0,\n",
    "        301.0,\n",
    "        0.0,\n",
    "        53.0,\n",
    "        0.0,\n",
    "        0.0,\n",
    "        0.0,\n",
    "        8,\n",
    "        6,\n",
    "        26.29,\n",
    "        10.92,\n",
    "        1000.0,\n",
    "        1.0,\n",
    "        500.0,\n",
    "        590.95,\n",
    "        0.0,\n",
    "        0.0,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        1,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        0,\n",
    "        1,\n",
    "        0,\n",
    "    ]\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6136a9ce",
   "metadata": {},
   "source": [
    "We can see that this application will be rejected with a probability of $\\sim77\\%$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "d3789d21",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[0.7770493  0.22295067]]\n",
      "Paid loan is predicted as: [False]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "print(xg_model.predict_proba(np.array(x)))\n",
    "print(f\"Paid loan is predicted as: {xg_model.predict(np.array(x))}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cfdf2010",
   "metadata": {},
   "source": [
    "We will now prepare the XGBoost model to be used from the TrustyAI counterfactual engine.\n",
    "\n",
    "To do so, we simply need to first create a prediction function which takes:\n",
    "\n",
    "- A `java.util.List` of `PredictionInput` as inputs\n",
    "- A `java.util.List` of `PredictionOutput` as outputs\n",
    "\n",
    "If these two conditions are met, the actual inner working of this method can be anything (including calling a XGBoost Python model for prediction as in our case):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "670f8d91",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import List\n",
    "from trustyai.utils import toJList\n",
    "\n",
    "\n",
    "def predict(inputs: List[PredictionInput]) -> List[PredictionOutput]:\n",
    "    values = [feature.getValue().asNumber() for feature in inputs.get(0).getFeatures()]\n",
    "    result = xg_model.predict_proba(np.array([values]))\n",
    "    false_prob, true_prob = result[0]\n",
    "    if false_prob > true_prob:\n",
    "        prediction = (False, false_prob)\n",
    "    else:\n",
    "        prediction = (True, true_prob)\n",
    "    output = Output(\"PaidLoan\", Type.BOOLEAN, Value(prediction[0]), prediction[1])\n",
    "    return toJList([PredictionOutput([output])])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1cb77932",
   "metadata": {},
   "source": [
    "Once the prediction method is created, we wrap in a `PredictionProvider` class.\n",
    "\n",
    "This class takes care of all the JVM's asynchronous plumbing for us."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "a4a35f43",
   "metadata": {},
   "outputs": [],
   "source": [
    "from trustyai.model import PredictionProvider\n",
    "\n",
    "model = PredictionProvider(predict)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0874edb",
   "metadata": {},
   "source": [
    "We will now express the previous inputs (`x`) in terms of `Feature`s, so that we might use it for the counterfactual search:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "69a52cf7",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_feature(name, value):\n",
    "    if type(value) is bool:\n",
    "        return FeatureFactory.newBooleanFeature(name, value)\n",
    "    else:\n",
    "        return FeatureFactory.newNumericalFeature(name, value)\n",
    "\n",
    "\n",
    "features = [\n",
    "    make_feature(p[0], p[1])\n",
    "    for p in [\n",
    "        (\"NewCreditCustomer\", False),\n",
    "        (\"Amount\", 2125.0),\n",
    "        (\"Interest\", 20.97),\n",
    "        (\"LoanDuration\", 60.0),\n",
    "        (\"Education\", 4.0),\n",
    "        (\"NrOfDependants\", 0.0),\n",
    "        (\"EmploymentDurationCurrentEmployer\", 6.0),\n",
    "        (\"IncomeFromPrincipalEmployer\", 0.0),\n",
    "        (\"IncomeFromPension\", 301.0),\n",
    "        (\"IncomeFromFamilyAllowance\", 0.0),\n",
    "        (\"IncomeFromSocialWelfare\", 53.0),\n",
    "        (\"IncomeFromLeavePay\", 0.0),\n",
    "        (\"IncomeFromChildSupport\", 0.0),\n",
    "        (\"IncomeOther\", 0.0),\n",
    "        (\"ExistingLiabilities\", 8.0),\n",
    "        (\"RefinanceLiabilities\", 6.0),\n",
    "        (\"DebtToIncome\", 26.29),\n",
    "        (\"FreeCash\", 10.92),\n",
    "        (\"CreditScoreEeMini\", 1000.0),\n",
    "        (\"NoOfPreviousLoansBeforeLoan\", 1.0),\n",
    "        (\"AmountOfPreviousLoansBeforeLoan\", 500.0),\n",
    "        (\"PreviousRepaymentsBeforeLoan\", 590.95),\n",
    "        (\"PreviousEarlyRepaymentsBefoleLoan\", 0.0),\n",
    "        (\"PreviousEarlyRepaymentsCountBeforeLoan\", 0.0),\n",
    "        (\"Council_house\", False),\n",
    "        (\"Homeless\", False),\n",
    "        (\"Joint_ownership\", False),\n",
    "        (\"Joint_tenant\", False),\n",
    "        (\"Living_with_parents\", False),\n",
    "        (\"Mortgage\", False),\n",
    "        (\"Other\", False),\n",
    "        (\"Owner\", False),\n",
    "        (\"Owner_with_encumbrance\", True),\n",
    "        (\"Tenant\", True),\n",
    "        (\"Entrepreneur\", False),\n",
    "        (\"Fully\", False),\n",
    "        (\"Partially\", False),\n",
    "        (\"Retiree\", True),\n",
    "        (\"Self_employed\", False),\n",
    "    ]\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c844bcd8",
   "metadata": {},
   "source": [
    "We can confirm now, with the newly created `PredictionProvider` model that this input will lead to a `false` `PaidLoan` prediction:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "60a46482",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Output{value=false, type=boolean, score=0.7835956811904907, name='PaidLoan'}'"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from trustyai.utils import toJList\n",
    "\n",
    "model.predictAsync(toJList([PredictionInput(features)])).get()[0].getOutputs()[\n",
    "    0\n",
    "].toString()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98e212fc",
   "metadata": {},
   "source": [
    "### Unconstraind basic search\n",
    "\n",
    "To get started we will search for a counterfactual with no constraints at all. This is not a realistic use case, but we will use it as a baseline."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "71b0c468",
   "metadata": {},
   "outputs": [],
   "source": [
    "n_features = len(features)\n",
    "\n",
    "constraints = [False] * n_features"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4fb48af",
   "metadata": {},
   "source": [
    "We will also create a set of equal bounds for all the features. Again, this is not realistic, but we do it to establish a baseline. Note that boolean features will ignore the bounds anyway, so we can just create a set such as:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "711b1b69",
   "metadata": {},
   "outputs": [],
   "source": [
    "features_boundaries = [NumericalFeatureDomain.create(0.0, 10000.0)] * n_features"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "608ae3ea",
   "metadata": {},
   "source": [
    "Next, we create a termination criteria for the search. We will use a 10 second time limit for the search:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "a12787eb",
   "metadata": {},
   "outputs": [],
   "source": [
    "termination_config = TerminationConfig().withSecondsSpentLimit(Long.valueOf(10))\n",
    "\n",
    "solver_config = (\n",
    "    CounterfactualConfigurationFactory.builder()\n",
    "    .withTerminationConfig(termination_config)\n",
    "    .build()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f0ca702e",
   "metadata": {},
   "source": [
    "We can now instantiate the counterfactual explainer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "ebc90e4a",
   "metadata": {},
   "outputs": [],
   "source": [
    "explainer = CounterfactualExplainer(solver_config)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb26b042",
   "metadata": {},
   "source": [
    "We want our **goal** to be the model predicting the loan will be paid (`PaidLoad=true`), so we specify it as:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "00e04f82",
   "metadata": {},
   "outputs": [],
   "source": [
    "goal = [Output(\"PaidLoan\", Type.BOOLEAN, Value(True), 0.0)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e45b88c0",
   "metadata": {},
   "source": [
    "We now wrap all this context in a `CounterfactualPrediction` object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "25603360",
   "metadata": {},
   "outputs": [],
   "source": [
    "original = PredictionInput(features)\n",
    "goals = PredictionOutput(goal)\n",
    "domain = PredictionFeatureDomain(features_boundaries)\n",
    "\n",
    "prediction = CounterfactualPrediction(\n",
    "    original, goals, domain, constraints, None, uuid.uuid4()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2aecad8d",
   "metadata": {},
   "source": [
    "We are now ready to search for a counterfactual:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "1d32c109",
   "metadata": {},
   "outputs": [],
   "source": [
    "explanation = explainer.explain(prediction, model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10cb34c1",
   "metadata": {},
   "source": [
    "First we will confirm that our counterfactual changes the outcome, by predicting its outcome using the model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "02a9527c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Output{value=true, type=boolean, score=0.6006738543510437, name='PaidLoan'}'"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "testf = [f.asFeature() for f in explanation.getEntities()]\n",
    "model.predictAsync(toJList([PredictionInput(testf)])).get()[0].getOutputs()[\n",
    "    0\n",
    "].toString()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1bcd184e",
   "metadata": {},
   "source": [
    "And indeed it changes. We will now verify _which_ features were changed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "cec4ab29",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Feature 'IncomeFromSocialWelfare': 53.0 -> 53.31125429433703\n",
      "Feature 'RefinanceLiabilities': 6.0 -> 1.230474777192958\n",
      "Feature 'PreviousEarlyRepaymentsCountBeforeLoan': 0.0 -> 6.0\n",
      "Feature 'Owner': false -> true\n",
      "Feature 'Owner_with_encumbrance': true -> false\n"
     ]
    }
   ],
   "source": [
    "def show_changes(explanation, original):\n",
    "    entities = explanation.getEntities()\n",
    "    N = len(original)\n",
    "    for i in range(N):\n",
    "        name = original[i].getName()\n",
    "        original_value = original[i].getValue()\n",
    "        new_value = entities[i].asFeature().getValue()\n",
    "        if original_value != new_value:\n",
    "            print(f\"Feature '{name}': {original_value} -> {new_value}\")\n",
    "\n",
    "\n",
    "show_changes(explanation, features)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bf923811",
   "metadata": {},
   "source": [
    "Here we can see the problem with the unconstrained search.\n",
    "\n",
    "Some of the fields that were changed (_e.g._ `IncomeFromSocialWelfare`, `RefinanceLiabilities`, etc) might be difficult to change in practice."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0af45ae5",
   "metadata": {},
   "source": [
    "### Constrained search\n",
    "\n",
    "We will now try a more realistic search, which incorporates domain specific knowledge (and common sence).\n",
    "\n",
    "To do so, we will constrain features we feel they shouldn't (or mustn't) change and specify sensible search bounds.\n",
    "We will start with the constraints:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "f7367eed",
   "metadata": {},
   "outputs": [],
   "source": [
    "constraints = [\n",
    "    True,  # NewCreditCustomer\n",
    "    False,  # Amount\n",
    "    True,  # Interest\n",
    "    False,  # LoanDuration\n",
    "    True,  # Education\n",
    "    True,  # NrOfDependants\n",
    "    False,  # EmploymentDurationCurrentEmployer\n",
    "    False,  # IncomeFromPrincipalEmployer\n",
    "    False,  # IncomeFromPension\n",
    "    False,  # IncomeFromFamilyAllowance\n",
    "    False,  # IncomeFromSocialWelfare\n",
    "    False,  # IncomeFromLeavePay\n",
    "    False,  # IncomeFromChildSupport\n",
    "    False,  # IncomeOther\n",
    "    True,  # ExistingLiabilities\n",
    "    True,  # RefinanceLiabilities\n",
    "    False,  # DebtToIncome\n",
    "    False,  # FreeCash\n",
    "    False,  # CreditScoreEeMini\n",
    "    True,  # NoOfPreviousLoansBeforeLoan\n",
    "    True,  # AmountOfPreviousLoansBeforeLoan\n",
    "    True,  # PreviousRepaymentsBeforeLoan\n",
    "    True,  # PreviousEarlyRepaymentsBefoleLoan\n",
    "    True,  # PreviousEarlyRepaymentsCountBeforeLoan\n",
    "    False,  # Council_house\n",
    "    False,  # Homeless\n",
    "    False,  # Joint_ownership\n",
    "    False,  # Joint_tenant\n",
    "    False,  # Living_with_parents\n",
    "    False,  # Mortgage\n",
    "    False,  # Other\n",
    "    False,  # Owner\n",
    "    False,  # Owner_with_encumbrance\"\n",
    "    False,  # Tenant\n",
    "    False,  # Entrepreneur\n",
    "    False,  # Fully\n",
    "    False,  # Partially\n",
    "    False,  # Retiree\n",
    "    False,  # Self_employed\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5d0f747",
   "metadata": {},
   "source": [
    "The constraints should be self-explanatory, but in essence they were divided into three groups\n",
    "\n",
    "- Attributes you **cannot** or **should** not change (protected), for instance age, education level, etc\n",
    "- Attributes you **can** change, for loan duration, loan amount, etc\n",
    "- Attributes you probably won't be able to change, but might be informative to change. For instance, you might not be able to easily change your income, but you might be interested in how much would it need to be in order to get the prediction as favourable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "1d166690",
   "metadata": {},
   "outputs": [],
   "source": [
    "features_boundaries = [\n",
    "    None,  # NewCreditCustomer\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # Amount\n",
    "    None,  # Interest\n",
    "    NumericalFeatureDomain.create(0.0, 120.0),  # LoanDuration\n",
    "    None,  # Education\n",
    "    None,  # NrOfDependants\n",
    "    NumericalFeatureDomain.create(0.0, 40.0),  # EmploymentDurationCurrentEmployer\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # IncomeFromPrincipalEmployer\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # IncomeFromPension\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # IncomeFromFamilyAllowance\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # IncomeFromSocialWelfare\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # IncomeFromLeavePay\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # IncomeFromChildSupport\n",
    "    NumericalFeatureDomain.create(0.0, 1000.0),  # IncomeOthe\n",
    "    None,  # ExistingLiabilities\n",
    "    None,  # RefinanceLiabilities\n",
    "    NumericalFeatureDomain.create(0.0, 100.0),  # DebtToIncome\n",
    "    NumericalFeatureDomain.create(0.0, 100.0),  # FreeCash\n",
    "    NumericalFeatureDomain.create(0.0, 10000.0),  # CreditScoreEeMini\n",
    "    None,  # NoOfPreviousLoansBeforeLoan\n",
    "    None,  # AmountOfPreviousLoansBeforeLoan\n",
    "    None,  # PreviousRepaymentsBeforeLoan\n",
    "    None,  # PreviousEarlyRepaymentsBefoleLoan\n",
    "    None,  # PreviousEarlyRepaymentsCountBeforeLoan\n",
    "    None,  # Council_house\n",
    "    None,  # Homeless\n",
    "    None,  # Joint_ownership\n",
    "    None,  # Joint_tenant\n",
    "    None,  # Living_with_parents\n",
    "    None,  # Mortgage\n",
    "    None,  # Other\n",
    "    None,  # Owner\n",
    "    None,  # Owner_with_encumbrance\n",
    "    None,  # Tenant\n",
    "    None,  # Entrepreneur\n",
    "    None,  # Fully\n",
    "    None,  # Partially\n",
    "    None,  # Retiree\n",
    "    None,  # Self_employed\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bdecdb9c",
   "metadata": {},
   "source": [
    "As before, we wrap this data in a `CounterfactualPrediction`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "91cc678b",
   "metadata": {},
   "outputs": [],
   "source": [
    "original = PredictionInput(features)\n",
    "goals = PredictionOutput(goal)\n",
    "domain = PredictionFeatureDomain(features_boundaries)\n",
    "\n",
    "prediction = CounterfactualPrediction(\n",
    "    original, goals, domain, constraints, None, uuid.uuid4()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d265b0d6",
   "metadata": {},
   "source": [
    "And we start a new search:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "fbce0a0b",
   "metadata": {},
   "outputs": [],
   "source": [
    "explanation = explainer.explain(prediction, model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20d7a6ca",
   "metadata": {},
   "source": [
    "We test that the counterfactual does change the outcome:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "f7d56e08",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Output{value=true, type=boolean, score=0.5300767421722412, name='PaidLoan'}'"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "testf = [f.asFeature() for f in explanation.getEntities()]\n",
    "model.predictAsync(toJList([PredictionInput(testf)])).get()[0].getOutputs()[\n",
    "    0\n",
    "].toString()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8968d072",
   "metadata": {},
   "source": [
    "And we confirm that no constrained features were changed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "bef56708",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Feature 'LoanDuration': 60.0 -> 56.947228037333545\n",
      "Feature 'IncomeFromSocialWelfare': 53.0 -> 60.0\n",
      "Feature 'FreeCash': 10.92 -> 10.914352713171315\n",
      "Feature 'Entrepreneur': false -> true\n"
     ]
    }
   ],
   "source": [
    "show_changes(explanation, features)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e2b413f3",
   "metadata": {},
   "source": [
    "### Minimum counterfactual probabilities\n",
    "\n",
    "We can see that the previous answer is very close to $50\\%$.\n",
    "\n",
    "With TrustyAI we have the possiblity to specify a minimum probability for the result (when the model supports prediction confidences).\n",
    "\n",
    "Let's say we want a result that is at least $75\\%$ confident that the loan will be repaid. We can just encode the **minimum probability** as the last argument of each `Output`. A minimum probability of $0$ (as we've used) simply means that any desired outcome will be accepted, regardless of its probability. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "e85f2ef2",
   "metadata": {},
   "outputs": [],
   "source": [
    "goal = [Output(\"PaidLoan\", Type.BOOLEAN, Value(True), 0.75)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9af0420",
   "metadata": {},
   "source": [
    "We can then re-run the search with all the data as defined previously:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "242718f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "original = PredictionInput(features)\n",
    "goals = PredictionOutput(goal)\n",
    "domain = PredictionFeatureDomain(features_boundaries)\n",
    "\n",
    "prediction = CounterfactualPrediction(\n",
    "    original, goals, domain, constraints, None, uuid.uuid4()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e48a29fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "explanation = explainer.explain(prediction, model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ea8abca",
   "metadata": {},
   "source": [
    "As previously, we check that the answer is what we are looking for"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04067b73",
   "metadata": {},
   "outputs": [],
   "source": [
    "testf = [f.asFeature() for f in explanation.getEntities()]\n",
    "model.predictAsync(toJList([PredictionInput(testf)])).get()[0].getOutputs()[\n",
    "    0\n",
    "].toString()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb1f68d8",
   "metadata": {},
   "source": [
    "And we show which features need to be changed for said desired outcome:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c39158a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_changes(explanation, features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca8e3ee6",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python-trustyai",
   "language": "python",
   "name": "python-trustyai"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
